﻿package {
	import at.geoathome.utils.lang.Language;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.*;
	import flash.display.DisplayObject;
	
	import at.geoathome.utils.loader.AssetLoader;
	
	import flash.display.Sprite;
	import flash.events.Event;	

	/**
	 * ...
	 * @author Georg Kamptner
	 */
	public class GameWorld extends Sprite
	{
		// -- variables & attributes ---------------------------------------------------------------------------
		protected var _game:Game;
		protected var _asset_loader:AssetLoader;
		protected var _width:int;
		protected var _height:int;
		protected var _conversion_value:Number; // the value which is used to multiply box2d values to represent pixels. (and pixel values are divided by to represent box2d values).
		//protected var _ground :b2Body; // the ground Body Object
		protected var _grounds : Array = new Array();
		protected var _ground_vertical_enlargement:int; // how much the ground should be extended on both sides relative to _width.
		protected var _canvas:Sprite;
		protected var _m_world:b2World;
		protected var _m_iterations:int; 	 // the accuracy, mor iterations means higher accuracty but less performance
		protected var _m_time_step:Number; 	 // timestep
		protected var _m_world_scale:Number; // tmiestep
		protected var _m_gravity:b2Vec2;
		protected var _add_to_of_display_list_on_update:Array;
		
		private var CHECK_POSITIONS:Boolean;
		
		public var debug:Boolean = false;
		
		public static const BORDER_OFFSET : Number = 0.05;
		
		// -- getter & setter ----------------------------------------------------------------------------------
		public function get grounds():Array{ return _grounds; }
		public function get check_positions():Boolean{return CHECK_POSITIONS;}
		
		// -- constructor --------------------------------------------------------------------------------------
		public function GameWorld(game:Game, width:int, height:int, gravity:b2Vec2, conversion_value:Number = 20, asset_loader:AssetLoader = null) 
		{
			_game						 = game;
			_width 						 = width;
			_height 					 = height;
			_conversion_value 			 = conversion_value;
			_m_gravity					 = gravity; 
			_asset_loader				 = asset_loader;
			_add_to_of_display_list_on_update = new Array();
			
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		
		// -- methods ------------------------------------------------------------------------------------------
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			
			// -- set initial values
			_canvas						 = new Sprite();
			addChild(_canvas);
			_ground_vertical_enlargement = 25; // how much the ground should be extended on both sides (in pixels, relative to _width).
			_m_iterations 			 	 = 14; // the accuracy, more iterations means higher accuracty but less performance
			_m_time_step 			 	 = 1.0 / 40; // timestep
			
			// -- start creating the Game World
			create2dboxWorld();
			
			if (_game.game_type == Game.GAME_TYPE_A)
			{
				addGround();
			}
			else
			{
				// add ground
				add2Grounds();
				//start update wenn state in "spiel mit grenze"
				startBoxPositionListener();
			}
		}
		
		
		public function startBoxPositionListener() : void
		{
			CHECK_POSITIONS=true;
			addEventListener(Event.ENTER_FRAME, checkBoxPositions);
		}
		
		public function stopBoxPositionListener() : void
		{
			CHECK_POSITIONS=false;
			removeEventListener(Event.ENTER_FRAME, checkBoxPositions);
		}
		
		private function checkBoxPositions(e:Event): void
		{	
			var aabb:b2AABB = new b2AABB();
			aabb.lowerBound.Set(0,0);
			aabb.upperBound.Set(convertPixelsToBox2d((_width/2)-50), convertPixelsToBox2d(_height));
			
			var aabb2:b2AABB = new b2AABB();
			aabb2.lowerBound.Set(convertPixelsToBox2d((_width/2)+50),0);
			aabb2.upperBound.Set(convertPixelsToBox2d(_width), convertPixelsToBox2d(_height));
			
			checkBoxPositionsForPlayerAndBoundingBox(0, aabb);
			checkBoxPositionsForPlayerAndBoundingBox(1, aabb2);
		}
		
		private function checkBoxPositionsForPlayerAndBoundingBox(player_id:int, aabb:b2AABB):void
		{
			var k_bufferSize:Number = 200;
			var buffer:Array = [];
			var count:Number = _m_world.Query(aabb, buffer, k_bufferSize);

			for (var i:Number = 0; i < count; ++i)
			{
				if (buffer[i] != undefined && buffer[i].GetBody().m_userData is IGameBody )
				{
					//schicke update message an box
					var body : b2Body= buffer[i].GetBody();
					var bodydrag : GameBodyDragable = body.m_userData;
					bodydrag.updatePlayerOwner(player_id);
				}
			}
		}
		
		
		private function convertPixelsToBox2d(value:int):Number
		{
			return Number(value / _conversion_value);
		}
		
		private function convertBox2dToPixels(value:Number):int
		{
			return int(value * _conversion_value);
		}
		
		private function create2dboxWorld():void
		{
			// Creat world AABB
			// the Ränder der Welt, triff ein Objekt auf diesen Rand wird es eingefroren und nimmt nicht mehr an der simulation Teil.
			var worldAABB:b2AABB = new b2AABB();
			worldAABB.lowerBound.Set(-100.0, -100.0);
			worldAABB.upperBound.Set(100.0, 100.0);
			
			// Define the gravity vector
			var gravity:b2Vec2 = _m_gravity;
			
			// Allow bodies to sleep
			var doSleep:Boolean = true;
			
			// Construct a world object
			// the brWorld is the Factory which generates all Objects.
			_m_world = new b2World(worldAABB, gravity, doSleep);
			
			if (debug)
			{
				// set debug draw
				var dbgDraw:b2DebugDraw = new b2DebugDraw();
				var dbgSprite:Sprite = new Sprite();
				_canvas.addChild(dbgSprite);
				dbgDraw.m_sprite = dbgSprite;
				dbgDraw.m_drawScale = _conversion_value;
				dbgDraw.m_fillAlpha = 0.0;
				dbgDraw.m_lineThickness = 1.0;
				dbgDraw.m_drawFlags = 0xFFFFFFFF;
				_m_world.SetDebugDraw(dbgDraw);
			}
		}
		
		private function addGround():void
		{
			// Vars used to create bodies
			var ground_def:b2BodyDef;
			var ground_shape:b2PolygonDef;
			
			// ground definition
			ground_def = new b2BodyDef();
			ground_def.position.Set( convertPixelsToBox2d(_width/2), convertPixelsToBox2d(_height) );
			ground_def.angle = 0.0;
			
			// ground shape
			ground_shape = new b2PolygonDef();
			ground_shape.SetAsBox( convertPixelsToBox2d(_width + 2 * _ground_vertical_enlargement) / 2, convertPixelsToBox2d(164) / 2 );
			ground_shape.friction = 0.3;
			ground_shape.density = 0; // static bodies require zero density(=Dichte)
						
			// add ground visual to ground definition
			ground_def.userData = new (_asset_loader.getDefinitionOf("PhysGround"))();
			ground_def.userData.width = _width + 2 * _ground_vertical_enlargement;
			ground_def.userData.height = 164; 
			
			// add ground decoration
			var decoration:Sprite = new (_asset_loader.getDefinitionOf("GroundDecoration"))() ;
			decoration.x = - ground_def.userData.width/2;
			decoration.y = - ground_def.userData.height/2; 
			ground_def.userData.addChild( decoration );
			
			_canvas.addChild(ground_def.userData);
			
			// body
			var ground : b2Body;
			ground = _m_world.CreateBody(ground_def);
			ground.CreateShape(ground_shape);
			ground.SetMassFromShapes();
			_grounds.push(ground);
		}
		
		private function add2Grounds():void
		{
			// Vars used to create bodies
			var ground_def:b2BodyDef;
			var ground_shape:b2PolygonDef;
			var ground_def2:b2BodyDef;
			var ground_shape2:b2PolygonDef;
			
			// ground definition
			ground_def = new b2BodyDef();
			ground_def.position.Set( convertPixelsToBox2d(_width/4-50), convertPixelsToBox2d(_height) );
			ground_def.angle = 0.0;
			
			// ground shape
			ground_shape = new b2PolygonDef();
			ground_shape.SetAsBox( convertPixelsToBox2d((_width/2-50) + 2 * _ground_vertical_enlargement) / 2, convertPixelsToBox2d(100) / 2 );
			ground_shape.friction = 0.3;
			ground_shape.density = 0; // static bodies require zero density(=Dichte)
						
			// add ground visual to ground definition
			ground_def.userData = new (_asset_loader.getDefinitionOf("PhysGroundLeft"))();
			ground_def.userData.width = (_width/2-50) + 2 * _ground_vertical_enlargement;
			ground_def.userData.height = 100; 
			
			
			
			_canvas.addChild(ground_def.userData);
			
			// body
			var ground_body : b2Body = _m_world.CreateBody(ground_def);
			ground_body.CreateShape(ground_shape);
			ground_body.SetMassFromShapes();
			_grounds.push(ground_body);
			
			
			// ground definition
			ground_def2 = new b2BodyDef();
			ground_def2.position.Set( convertPixelsToBox2d((_width/4)*3+50), convertPixelsToBox2d(_height) );
			ground_def2.angle = 0.0;
			
			// ground shape
			ground_shape2 = new b2PolygonDef();
			ground_shape2.SetAsBox( convertPixelsToBox2d(((_width/4)*3+50) + 2 * _ground_vertical_enlargement) / 2, convertPixelsToBox2d(100) / 2 );
			ground_shape2.friction = 0.3;
			ground_shape2.density = 0; // static bodies require zero density(=Dichte)
						
			// add ground visual to ground definition
			ground_def2.userData = new (_asset_loader.getDefinitionOf("PhysGroundRight"))();
			ground_def2.userData.width = (_width/2-50) + 2 * _ground_vertical_enlargement;
			ground_def2.userData.height = 100;  
			
			_canvas.addChild(ground_def2.userData);
			
			// add ground decoration
			var decoration:Sprite = new (_asset_loader.getDefinitionOf("GroundDecorationLeftRight"))() ;
			decoration.x = _width / 2;
			decoration.y = _height - decoration.height;
			_canvas.addChild( decoration );
			
			_add_to_of_display_list_on_update.push(decoration);
			
			// body
			var ground_body2:b2Body = _m_world.CreateBody(ground_def2);
			ground_body2.CreateShape(ground_shape);
			ground_body2.SetMassFromShapes();
			_grounds.push(ground_body2);
		}
		
		
		private function bringGroundUserDataToFront():void
		{
			for (var k:int = 0 ; k < _grounds.length; ++k){
				_canvas.addChild(_grounds[k].GetUserData());
			}
		}
		
		public function addBox(asset_classname:String, width:int, height:int, friction:Number = 0.3, density:Number = 1.7, angle:Number = 0.0, x:Number = Number.NaN, y : Number = Number.NaN):b2Body
		{
			// params
			if(isNaN(x))
				x = stage.mouseX - this.x;
			if(isNaN(y))
				y = stage.mouseY - this.y;
				
			// Vars used to create bodies
			var body:b2Body;
			var box_def:b2BodyDef;
			var box_shape:b2PolygonDef;
			
			// ground definition
			box_def = new b2BodyDef();
			box_def.position.Set( convertPixelsToBox2d(x), convertPixelsToBox2d(y) );
			box_def.angle = angle;
			
			// ground shape
			box_shape = new b2PolygonDef();
			box_shape.SetAsBox( convertPixelsToBox2d(width) / 2, convertPixelsToBox2d(height) / 2 );
			box_shape.friction = friction;
			box_shape.density = density;
						
			// add ground visual to ground definition
			box_def.userData = new GameBodyDragable(null, _game, _asset_loader.getDefinitionOf(asset_classname));
			box_def.userData.width = width;
			box_def.userData.height = height; 
			
			_canvas.addChild(box_def.userData);
			
			// body
			body = _m_world.CreateBody(box_def);
			body.CreateShape(box_shape);
			body.SetMassFromShapes();
			
			// bring ground decoration to front
			bringGroundUserDataToFront();
			
			// set body in GameBody
			box_def.userData.body = body;
			return body;
		}
		
		public function removeBox(body:b2Body):void
		{
			if(_canvas.contains(body.m_userData))
				_canvas.removeChild(body.m_userData);
			
			_m_world.DestroyBody(body);
		}
		
		public function addCircle(radius:int, mass:int, rotation:Number):void
		{
			
		}
		
		public function startSimulation(e:Event = null):void
		{
			// Add event for main loop
			addEventListener(Event.ENTER_FRAME, _update, false, 0, true);
		}
		
		public function stopSimulation(e:Event = null):void
		{
			// remove main loop Event
			removeEventListener(Event.ENTER_FRAME, _update, false);
		}
		
		private function _update(e:Event):void
		{	
			_m_world.Step(_m_time_step, _m_iterations);
			
			// Go through body list and update sprite positions/rotations
			for (var bb:b2Body = _m_world.m_bodyList; bb; bb = bb.m_next)
			{
				if (bb.m_userData is Sprite)
				{
					bb.m_userData.x = convertBox2dToPixels( bb.GetPosition().x );
					bb.m_userData.y = convertBox2dToPixels( bb.GetPosition().y );
					bb.m_userData.rotation = bb.GetAngle() * (180/Math.PI);
				}
			}
			
			var obj:DisplayObject;
			for each(obj in _add_to_of_display_list_on_update)
			{
				_canvas.addChild(obj);
			}
		}
		
		public function updateBodyPositionInPixels(body:b2Body, x:int, y:int, vx:int = 0, vy:int = 0):void
		{
			body.SetXForm( new b2Vec2( convertPixelsToBox2d(x), convertPixelsToBox2d(y) ), 0);
			body.SetLinearVelocity( new b2Vec2( convertPixelsToBox2d(vx), convertPixelsToBox2d(vy) ) );
			//bb.m_userData.rotation = bb.GetAngle() * (180/Math.PI);
		}
		
		public function getBodiesAroundBody(body:b2Body):Array
		{	
			var aabb:b2AABB = new b2AABB();
			aabb.lowerBound.Set(body.GetPosition().x  - 0.8, body.GetPosition().y  - 0.8);
			aabb.upperBound.Set(body.GetPosition().x  + 0.8, body.GetPosition().y  + 0.8);
			var k_bufferSize:Number = 30;
			var buffer:Array = [];
			var count:Number = _m_world.Query(aabb, buffer, k_bufferSize);
			
			// build return value
			var ret:Array = new Array();
			for (var i:Number = 0; i < count; ++i)
			{
				if (buffer[i].GetBody() == body)
					continue;
				ret.push(buffer[i].GetBody());
			}
			
			return ret;
		}
		
		public function getBodiesOnTopOfBody(body:b2Body, player:Player = null):Array
		{	
			var check_border_distance:Number = BORDER_OFFSET;
			var body_width_h:Number = convertPixelsToBox2d(body.m_userData.width) / 2;
			var body_height_h:Number = convertPixelsToBox2d(body.m_userData.height) / 2;
			var check_distance_x:Number = body_width_h + check_border_distance;
			var check_distance_y:Number = body_height_h + check_border_distance;
			
			var aabb:b2AABB = new b2AABB();
			aabb.lowerBound.Set(body.GetPosition().x  - check_distance_x, body.GetPosition().y  - check_distance_y);
			aabb.upperBound.Set(body.GetPosition().x  + check_distance_x, body.GetPosition().y  + check_distance_y);
			var k_bufferSize:Number = 30;
			var buffer:Array = [];
			var count:Number = _m_world.Query(aabb, buffer, k_bufferSize);
			
			// build return value
			var ret:Array = new Array();
			for (var i:Number = 0; i < count; ++i)
			{
				// remove the body itself
				if (buffer[i].GetBody() == body)
					continue;
				// remove all bodies which are not one of the spicified player, if player has been set, ignore this setting
				if (player != null && buffer[i].GetBody().m_userData.player != player)
					continue
				// remove all bodies which are allready checked
				if (buffer[i].GetBody().m_userData.wincheck_count > 0)
					continue;
				
				// buffer[i].GetBody().GetUserData().alpha = 0.5; // DEBUG
				buffer[i].GetBody().GetUserData().wincheck_count = 1;
				ret.push(buffer[i].GetBody());
			}
			
			return ret;
		}
		
		public function getBodiesOnTopOfGround(player:Player):Array
		{
			if (_grounds.length<2){
				return getBodiesOnTopOfBody(_grounds[0], player);
			}
			if (_grounds.length>=2 && player.id == 0){
				return getBodiesOnTopOfBody(_grounds[0], player);
			}
			if (_grounds.length>=2 && player.id == 1){
				return getBodiesOnTopOfBody(_grounds[1], player);
			}
			//TODO nicht die beste lösung
			return null;
		}
		
		public function getTowerTops(ground:b2Body, player:Player = null, stack:Array = null, tops:Array = null):Array
		{
			if (tops == null)
				tops = new Array();
			
			if (stack == null)
				stack = new Array();
				
			stack.push(ground);
			
			var bodies:Array = getBodiesOnTopOfBody(ground, player);
			var body:b2Body;
			for each(body in bodies)
			{
				getTowerTops(body, player, stack.slice(), tops);
			}
			
			if (bodies.length == 0)
			{
				//ground.m_userData.alpha = 1.0; // DEBUG
				tops.push(stack);
			}
			
			return tops;
		}
		
		public function getHighestTowerOfPlayer(player:Player = null):Array
		{
			var tops:Array;
			if(_grounds.length<2)
			{
				tops = getTowerTops(_grounds[0],player,null,null);
			}
			else{
				if (player.id == 0){
					tops = getTowerTops(_grounds[0], player, null, null);
				}
				else{
					tops = getTowerTops(_grounds[1], player, null, null);
				}
			}
			
			
			// fetch the highest tower of all tops
			if (tops.length > 0)
			{
				var absolute_top:Array = tops[0];
				var current_top:Array;
				var body:b2Body;
				for each(current_top in tops)
				{
					if (current_top[current_top.length - 1].GetPosition().y < absolute_top[absolute_top.length - 1].GetPosition().y)
						absolute_top = current_top;
					
					for each(body in current_top)
						body.m_userData.wincheck_count = 0;
				}
				
				return absolute_top;
			}
			
			return null;
		}
		
		public function getVerticalDistanceToGroundInPixels(body:b2Body, player:Player=null):int
		{
			// break conditions
			if (_grounds == null) return Number.NaN;
			if (body == null) return Number.NaN;
			if (_grounds.length < 2){
				return convertBox2dToPixels(_grounds[0].GetPosition().y - body.GetPosition().y);
			}
			else{
				if (player.id == 0){
					return convertBox2dToPixels(_grounds[0].GetPosition().y - body.GetPosition().y);
				}
				if (player.id == 1){
					return convertBox2dToPixels(_grounds[1].GetPosition().y - body.GetPosition().y);
				}
			}
			//TODO nicht die beste lösung
			return 0;
		}
		
		public function getHorizontalDistanceToLeftBorderInPixels(body:b2Body, player:Player=null):int
		{
			// break conditions
			if (_grounds == null) return Number.NaN;
			if (body == null) return Number.NaN;
			if (_grounds.length < 2){
				return convertBox2dToPixels(body.GetPosition().x);
			}
			else{
				if (player.id == 0){
					return convertBox2dToPixels(body.GetPosition().x);
				}
				if (player.id == 1){
					return convertBox2dToPixels( body.GetPosition().x);
				}
			}
			//TODO nicht die beste lösung
			return 0;
		}
		
		public function removeAllBodies():void
		{
			var i:int = 0;
			var l:int = _grounds.length;
			for ( ; i < l; ++i )
			{
				b2Body(_grounds[i]).DestroyShape(b2Body(_grounds[i]).GetShapeList());
				_game.world.removeBox(_grounds[i]);
			}
			
			// remove all on box2d objecs from canvas
			var obj:DisplayObject;
			for each(obj in _add_to_of_display_list_on_update)
			{
				_canvas.removeChild(obj);
				obj = null;
			}
			_add_to_of_display_list_on_update = new Array();
		}
		
	}
	
}